package com.caius.xiaohashu.note.biz.service.Impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import com.caius.framework.biz.context.holder.LoginUserContextHolder;
import com.caius.framework.common.exception.BizException;
import com.caius.framework.common.response.Response;
import com.caius.framework.common.util.DateUtils;
import com.caius.framework.common.util.JsonUtils;
import com.caius.xiaohashu.note.biz.domain.dataobject.NoteCollectionDO;
import com.caius.xiaohashu.note.biz.domain.dataobject.NoteDO;
import com.caius.xiaohashu.note.biz.domain.dataobject.NoteLikeDO;
import com.caius.xiaohashu.note.biz.domain.mapper.NoteCollectionDOMapper;
import com.caius.xiaohashu.note.biz.domain.mapper.NoteDOMapper;
import com.caius.xiaohashu.note.biz.domain.mapper.NoteLikeDOMapper;
import com.caius.xiaohashu.note.biz.domain.mapper.TopicDOMapper;
import com.caius.xiaohashu.note.biz.enums.*;
import com.caius.xiaohashu.note.biz.model.dto.CollectUnCollectNoteMqDTO;
import com.caius.xiaohashu.note.biz.model.dto.LikeUnlikeNoteReqDTO;
import com.caius.xiaohashu.note.biz.model.dto.NoteOperateMqDTO;
import com.caius.xiaohashu.note.biz.model.vo.*;
import com.caius.xiaohashu.note.biz.rcp.DistributedIdGeneratorRcpService;
import com.caius.xiaohashu.note.biz.rcp.KeyValueRcpService;
import com.caius.xiaohashu.note.biz.rcp.UserRcpService;
import com.caius.xiaohashu.note.biz.service.NoteService;
import com.caius.xiaohashu.user.dto.resp.FindUserByIdRspDTO;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import constant.MQConstants;
import constant.RedisKeyConstants;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

/**
 * @author Caius
 * @description 笔记业务
 * @since Created in 2025-05-20
 */
@Service
@Slf4j
public class NoteServiceImpl implements NoteService {

    @Resource
    private NoteDOMapper noteDOMapper;

    @Resource
    private TopicDOMapper topicDOMapper;

    @Resource
    private DistributedIdGeneratorRcpService distributedIdGeneratorRcpService;

    @Resource
    private KeyValueRcpService keyValueRcpService;

    @Resource
    private UserRcpService userRcpService;

    @Resource(name = "taskExecutor")
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @Resource
    private RocketMQTemplate rocketMQTemplate;

    @Resource
    private NoteLikeDOMapper noteLikeDOMapper;
    @Autowired
    private NoteCollectionDOMapper noteCollectionDOMapper;

    /**
     * 发布笔记
     * @param publishNoteReqVO
     * @return
     */
    @Override
    public Response<?> publishNote(PublishNoteReqVO publishNoteReqVO) {
        Integer type = publishNoteReqVO.getType();

        NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(type);

        if (Objects.isNull(noteTypeEnum)) {
            throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);
        }

        String imgUris = null;
        Boolean isContentEmpty = true;
        String videoUri = null;

        switch (noteTypeEnum) {
            case IMAGE_TEXT: // 图文
                List<String> imgUrisList = publishNoteReqVO.getImgUris();
                // 图片判空
                Preconditions.checkArgument(CollUtil.isNotEmpty(imgUrisList), "笔记本图片不能为空");
                // 图片数量校验
                Preconditions.checkArgument(imgUrisList.size() <= 8, "笔记本的图片不能多于 8 张");

                imgUris = StringUtils.join(imgUrisList, ",");

                break;
            case VIDEO:
                videoUri = publishNoteReqVO.getVideoUri();

                // 视频内容判空
                Preconditions.checkArgument(StringUtils.isNotBlank(videoUri), "笔记视频不能为空");

                break;
            default:
                break;
        }

        // RPC: 调用分布式 ID 生成服务，生成笔记 ID
        String snowflakeId = distributedIdGeneratorRcpService.getSnowflakeId();

        String contentUuid = null;

        String content = publishNoteReqVO.getContent();

        if (StringUtils.isNotBlank(content)) {
            isContentEmpty = false;

            contentUuid = UUID.randomUUID().toString();

            // RPC: 调用 KV 键值服务，存储短文本
            boolean isSaveSuccess = keyValueRcpService.saveNoteContent(contentUuid, content);

            if (!isSaveSuccess) {
                throw new BizException(ResponseCodeEnum.NOTE_PUBLISH_FAIL);
            }
        }

        Long topicId = publishNoteReqVO.getTopicId();
        String topicName = null;

        if (Objects.nonNull(topicId)) {
            topicName = topicDOMapper.selectNameByPrimaryKey(topicId);
        }

        Long creatorId = LoginUserContextHolder.getUserId();

        // 构建发布笔记对象
        NoteDO noteDO = NoteDO.builder()
                .id(Long.valueOf(snowflakeId))
                .isContentEmpty(isContentEmpty)
                .creatorId(creatorId)
                .imgUris(imgUris)
                .title(publishNoteReqVO.getTitle())
                .topicId(topicId)
                .topicName(topicName)
                .type(type)
                .visible(NoteVisiableEnum.PUBLIC.getCode())
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .status(NoteStatusEnum.NORMAL.getCode())
                .isTop(Boolean.FALSE)
                .videoUri(videoUri)
                .contentUuid(contentUuid)
                .build();

        try {
            noteDOMapper.insert(noteDO);
        } catch (Exception e) {
            log.error("==> 笔记存储失败", e);

            // RCP : 笔记保存失败，删除笔记内容
            if (StringUtils.isNotBlank(contentUuid)) {
                keyValueRcpService.deleteNoteContent(contentUuid);
            }
        }

        // 发送 MQ
        NoteOperateMqDTO noteOperateMqDTO = NoteOperateMqDTO.builder()
                .creatorId(creatorId)
                .noteId(Long.valueOf(snowflakeId))
                .type(NoteOperateEnum.PUBLISH.getCode())
                .build();

        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(noteOperateMqDTO))
                .build();


        String destination = MQConstants.TOPIC_NOTE_OPERATE + ":" + MQConstants.TAG_NOTE_PUBLISH;

        rocketMQTemplate.asyncSend(destination, message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 【笔记发布】MQ 发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 【笔记发布】MQ 发送异常: ", throwable);
            }
        });

        return Response.success();
    }


    /**
     * 笔记详情信息本地缓存
     */
    private static final Cache<Long, String> LOCAL_CACHE = Caffeine.newBuilder()
            .initialCapacity(10000)
            .maximumSize(10000)
            .expireAfterWrite(1, TimeUnit.HOURS)
            .build();

    /**
     * 笔记详情
     * @param findNoteDetailReqVO
     * @return
     */
    @Override
    @SneakyThrows
    public Response<FindNoteDetailRspVO> findNoteDetail(FindNoteDetailReqVO findNoteDetailReqVO) {
        Long noteId = findNoteDetailReqVO.getId();

        Long userId = LoginUserContextHolder.getUserId();

        String findNoteDetailRspVOStrLocalCache = LOCAL_CACHE.getIfPresent(noteId);
        if (StringUtils.isNotBlank(findNoteDetailRspVOStrLocalCache)) {
            FindNoteDetailRspVO findNoteDetailRspVO = JsonUtils.parseObject(findNoteDetailRspVOStrLocalCache, FindNoteDetailRspVO.class);
            log.info("==> 命中了本地缓存：{}", findNoteDetailRspVOStrLocalCache);
            // 可见性校验
            checkNoteVisibleFromVO(userId, findNoteDetailRspVO);
            return Response.success(findNoteDetailRspVO);
        }

        //从 redis 缓存中间获取
        String noteDetailKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        String noteDetailJson = redisTemplate.opsForValue().get(noteDetailKey);

        // 如果 redis 中间数据存在
        if (StringUtils.isNotBlank(noteDetailJson)) {
            FindNoteDetailRspVO findNoteDetailRspVO = JsonUtils.parseObject(noteDetailJson, FindNoteDetailRspVO.class);
            threadPoolTaskExecutor.submit(()->{
                // 写入本地缓存
                LOCAL_CACHE.put(noteId,
                        Objects.isNull(findNoteDetailRspVO) ? "null" : JsonUtils.toJsonString(findNoteDetailRspVO));
            });

            checkNoteVisibleFromVO(userId, findNoteDetailRspVO);

            return Response.success(findNoteDetailRspVO);
        }

        NoteDO noteDO = noteDOMapper.selectByPrimaryKey(noteId);

        if (Objects.isNull(noteDO)) {
            threadPoolTaskExecutor.execute(()->{
                // 防止缓存穿透，将空数据存入 Redis 缓存 (过期时间不宜设置过长)
                // 保底1分钟 + 随机秒数
                long expireSeconds = 60 + RandomUtil.randomInt(60);
                redisTemplate.opsForValue().set(noteDetailKey, "null", expireSeconds, TimeUnit.SECONDS);

            });
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }

        // 可见性校验
        Integer visible = noteDO.getVisible();
        checkNoteVisible(visible, userId, noteDO.getCreatorId());

        // RCP: 查询用户角色
        // 并发优化
        Long creatorId = noteDO.getCreatorId();
        CompletableFuture<FindUserByIdRspDTO> userResultFuture = CompletableFuture
                .supplyAsync(() -> userRcpService.findById(creatorId), threadPoolTaskExecutor);

        // RCP: 获取笔记内容
        // 并发优化
        CompletableFuture<String> contentResultFuture = CompletableFuture.completedFuture(null);
        if (Objects.equals(noteDO.getIsContentEmpty(), Boolean.FALSE)) {
             contentResultFuture = CompletableFuture.
                    supplyAsync(() -> keyValueRcpService.findNoteContent(noteDO.getContentUuid()), threadPoolTaskExecutor);
        }

        CompletableFuture<String> finalContentResultFuture = contentResultFuture;
        CompletableFuture<FindNoteDetailRspVO> resultFuture = CompletableFuture
                .allOf(userResultFuture, contentResultFuture)
                .thenApply(s -> {
                    // 获取 future 返回的结果
                    FindUserByIdRspDTO findUserByIdRspDTO = userResultFuture.join();
                    String content = finalContentResultFuture.join();

                    Integer noteType = noteDO.getType();
                    String imgUrisStr = noteDO.getImgUris();
                    List<String> imgUris = null;
                    // 获取图文信息里面呢的图片链接
                    if (Objects.equals(noteType, NoteTypeEnum.IMAGE_TEXT.getCode())
                            && StringUtils.isNotBlank(imgUrisStr)) {
                        imgUris = List.of(imgUrisStr.split(","));
                    }

                    return FindNoteDetailRspVO.builder()
                            .id(noteDO.getId())
                            .type(noteDO.getType())
                            .title(noteDO.getTitle())
                            .content(content)
                            .imgUris(imgUris)
                            .topicId(noteDO.getTopicId())
                            .topicName(noteDO.getTopicName())
                            .creatorId(noteDO.getCreatorId())
                            .creatorName(findUserByIdRspDTO.getNickName())
                            .avatar(findUserByIdRspDTO.getAvatar())
                            .videoUri(noteDO.getVideoUri())
                            .updateTime(noteDO.getUpdateTime())
                            .visible(noteDO.getVisible())
                            .build();
                });


        FindNoteDetailRspVO findNoteDetailRspVO = resultFuture.get();

        // 异步将笔记存入 redis 中间
        threadPoolTaskExecutor.submit(()->{
            String noteDetailJson1 = JsonUtils.toJsonString(findNoteDetailRspVO);
            // 过期时间（保底1天 + 随机秒数，将缓存过期时间打散，防止同一时间大量缓存失效，导致数据库压力太大）
            long expireSeconds  = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
            redisTemplate.opsForValue().set(noteDetailKey, noteDetailJson1, expireSeconds, TimeUnit.SECONDS);
        });

        return Response.success(findNoteDetailRspVO);
    }

    /**
     * 笔记更新
     * @param updateNoteReqVO
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Response<?> updateNote(UpdateNoteReqVO updateNoteReqVO) {
        Long noteId = updateNoteReqVO.getId();
        Integer type = updateNoteReqVO.getType();

        NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(type);
        if (Objects.isNull(noteTypeEnum)) {
            throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);
        }
        String imgUri = null;
        String videoUri = null;

        switch (noteTypeEnum) {
            case IMAGE_TEXT:
                List<String> imgUrisList = updateNoteReqVO.getImgUris();

                Preconditions.checkArgument(CollUtil.isNotEmpty(imgUrisList), "笔记图片不能为空");

                Preconditions.checkArgument(imgUrisList.size() <= 8, "笔记图片不能多于八张");

                imgUri = StringUtils.join(imgUrisList, ",");

                break;
            case VIDEO:
                videoUri = updateNoteReqVO.getVideoUri();

                Preconditions.checkArgument(StringUtils.isNotBlank(videoUri), "笔记视频不能为空");

                break;
            default:
                break;
        }

        Long currUserId = LoginUserContextHolder.getUserId();
        NoteDO selectNoteDO = noteDOMapper.selectByPrimaryKey(noteId);

        if (Objects.isNull(selectNoteDO)) {
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }
        // 非创作者不能修改笔记
        if (!Objects.equals(currUserId, selectNoteDO.getCreatorId())) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }

        Long topicId = updateNoteReqVO.getTopicId();
        String topicName = null;
        if (Objects.nonNull(topicId)) {
            topicName = topicDOMapper.selectNameByPrimaryKey(topicId);

            if (StringUtils.isBlank(topicName)) throw new BizException(ResponseCodeEnum.TOPIC_NOT_FOUND);
        }

        // 删除 redis 缓存
        String noteDetailKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailKey);

        // 更新 content 元数据表 t_note
        String content = updateNoteReqVO.getContent();
        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .isContentEmpty(StringUtils.isBlank(content))
                .imgUris(imgUri)
                .title(updateNoteReqVO.getTitle())
                .topicId(updateNoteReqVO.getTopicId())
                .topicName(topicName)
                .type(type)
                .updateTime(LocalDateTime.now())
                .videoUri(videoUri)
                .build();
        noteDOMapper.updateByPrimaryKey(noteDO);

        // 保证一致性使用 MQ 实现延迟双删
        Message<String> message = MessageBuilder.withPayload(String.valueOf(noteId)).build();

        rocketMQTemplate.asyncSend(MQConstants.TOPIC_DELAY_DELETE_NOTE_REDIS_CACHE, message,
                new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        log.info("## 延迟删除 redis 笔记缓存消息发送成功...");
                    }

                    @Override
                    public void onException(Throwable e) {
                        log.info("## 延迟删除 redis 笔记缓存消息发送失败...", e);
                    }
                },
                3000, // 超时时间 3s
                1  // 延迟级别，1 表示延时 1s
        );

        // 同步发送广播模式 MQ, 将所有实例中的本地缓存都删除掉
        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ: 删除笔记本地缓存发送成功...");

        // 检查更新
        NoteDO updateNoteDO = noteDOMapper.selectByPrimaryKey(noteId);
        String contentUuid = updateNoteDO.getContentUuid();

        boolean isUpdateContentSuccess = false;
        if (StringUtils.isBlank(content)) {
            isUpdateContentSuccess = keyValueRcpService.deleteNoteContent(contentUuid);
        } else {
            // 若将无内容的笔记，更新为了有内容的笔记，需要重新生成 UUID
            contentUuid = StringUtils.isBlank(contentUuid) ? UUID.randomUUID().toString() : contentUuid;
            // 更新文本
            isUpdateContentSuccess = keyValueRcpService.saveNoteContent(contentUuid, content);
        }

        if (!isUpdateContentSuccess) {
            throw new BizException(ResponseCodeEnum.NOTE_UPDATE_FAIL);
        }

        return Response.success();
    }

    /**
     * 删除本地缓存
     * @param noteId
     */
    @Override
    public void deleteNoteLocalCache(Long noteId) {
        LOCAL_CACHE.invalidate(noteId);
    }

    /**
     * 删除笔记
     * @param deleteNoteReqVO
     * @return
     */
    @Override
    public Response<?> deleteNote(DeleteNoteReqVO deleteNoteReqVO) {
        Long noteId = deleteNoteReqVO.getId();

        NoteDO selectNoteDO = noteDOMapper.selectByPrimaryKey(noteId);

        if (Objects.isNull(selectNoteDO)) {
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }

        // 删除权限判定
        Long currUserId = LoginUserContextHolder.getUserId();
        if (!Objects.equals(currUserId, selectNoteDO.getCreatorId())) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }

        // 逻辑删除
        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .status(NoteStatusEnum.DELETED.getCode())
                .updateTime(LocalDateTime.now())
                .build();

        int count = noteDOMapper.updateByPrimaryKey(noteDO);

        if (count == 0) {
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }

        // 缓存删除
        String noteDetailKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(String.valueOf(noteDetailKey));

        // MQ 删除本地缓存
        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ: 删除笔记本地缓存信息发送成功");

        NoteOperateMqDTO noteOperateMqDTO = NoteOperateMqDTO.builder()
                .creatorId(selectNoteDO.getCreatorId())
                .noteId(noteId)
                .type(NoteOperateEnum.DELETE.getCode())
                .build();

        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(noteOperateMqDTO))
                .build();

        String destination = MQConstants.TOPIC_NOTE_OPERATE + ":" + MQConstants.TAG_NOTE_DELETE;

        rocketMQTemplate.asyncSend(destination, message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 【笔记删除】MQ 发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 【笔记删除】MQ 发送异常: ", throwable);
            }
        });

        return Response.success();
    }

    /**
     * 修改笔记可见范围
     * @param updateNoteVisibleOnlyMeReqVO
     * @return
     */
    @Override
    public Response<?> visibleOnlyMe(UpdateNoteVisibleOnlyMeReqVO updateNoteVisibleOnlyMeReqVO) {
        Long noteId = updateNoteVisibleOnlyMeReqVO.getId();

        NoteDO selectNoteDO = noteDOMapper.selectByPrimaryKey(noteId);

        if (Objects.isNull(selectNoteDO)) {
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }

        Long currUserId = LoginUserContextHolder.getUserId();

        // 逻辑校验操作
        if (!Objects.equals(currUserId, selectNoteDO.getCreatorId())) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }

        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .visible(NoteVisiableEnum.PRIVATE.getCode())
                .updateTime(LocalDateTime.now())
                .build();

        int count = noteDOMapper.updateVisibleOnlyMe(noteDO);

        if (count == 0) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_VISIBLE_ONLY_ME);
        }

        String noteDetailKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailKey);

        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ 删除本地缓存信息发送成功...");

        return Response.success();
    }

    /**
     * 将笔记置顶
     * @param topNoteReqVO
     * @return
     */
    @Override
    public Response<?> topNote(TopNoteReqVO topNoteReqVO) {
        Long noteId = topNoteReqVO.getId();
        boolean isTop = topNoteReqVO.isTop();

        NoteDO selectNoteDO = noteDOMapper.selectByPrimaryKey(noteId);

        if (Objects.isNull(selectNoteDO)) {
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }

        Long currUserId = LoginUserContextHolder.getUserId();

        // 权限检验
        if (Objects.equals(currUserId, selectNoteDO.getCreatorId())) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }

        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .isTop(isTop)
                .creatorId(currUserId)
                .updateTime(LocalDateTime.now())
                .build();

        int count = noteDOMapper.updateByPrimaryKey(noteDO);

        if (count == 0) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }

        String noteDetailKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailKey);

        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ 删除本地缓存信息发送成功...");

        return Response.success();
    }

    /**
     * 点赞笔记
     * @param likeNoteReqVO
     * @return
     */
    @Override
    public Response<?> likeNote(LikeNoteReqVO likeNoteReqVO) {
        // 1. 校验被点赞的笔记是否存在
        Long noteId = likeNoteReqVO.getId();
        Long creatorId = checkNoteIsExistAndGetCreatorId(noteId);
        // 2. 判断目标笔记，是否已经点赞过
        Long userId = LoginUserContextHolder.getUserId();
        String bloomUserNoteLikeListRedisKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId);

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_like_check.lua")));
        script.setResultType(Long.class);

        Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListRedisKey), noteId);

        NoteLikeLuaResultEnum noteLikeLuaResultEnum = NoteLikeLuaResultEnum.valueOf(result);

        String userNoteLikeZSetRedisKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);

        switch (noteLikeLuaResultEnum){
            case NOT_EXIST -> {
                // 从数据库中校验笔记是否被点赞，并异步初始化布隆过滤器，设置过期时间 rcp 直接调用 note-relation 的 接口服务实现
                int count = noteLikeDOMapper.selectCountByUserIdAndNoteId(userId, noteId);

                long expiredSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);

                if (count > 0) {
                    // 初始化布隆过滤器
                    threadPoolTaskExecutor.submit(() -> {
                        batchAddNoteLike2BloomAndExpire(userId, expiredSeconds, bloomUserNoteLikeListRedisKey);
                        throw new BizException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
                    });

                }

                // 若目标笔记未被点赞，查询当前用户是否有点赞其他笔记，有则同步初始化布隆过滤器
                batchAddNoteLike2BloomAndExpire(userId, expiredSeconds, bloomUserNoteLikeListRedisKey);

                // 添加当前点赞笔记 ID 到布隆过滤器中
                // Lua 脚本路径
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_add_note_like_and_expire.lua")));
                script.setResultType(Long.class);
                redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListRedisKey), noteId, expiredSeconds);
            }
            case NOTE_LIKED -> {
                List<Double> score = redisTemplate.opsForZSet().score(userNoteLikeZSetRedisKey);
                if (Objects.nonNull(score)) {
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
                }
                // 若 Score 为空，则表示 ZSet 点赞列表中不存在，查询数据库校验
                int count = noteLikeDOMapper.selectNoteIsLike(userId, noteId);
                if (count > 0) {
                    // 异步初始化 ZSet =》 可能出现数据库中间存在数据但是 redis 的 ZSet 是 -1 无法初始化导致每次请求直接打到数据库
                    asynInitUserNoteLikesZSet(userId, userNoteLikeZSetRedisKey);

                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
                }
            }
        }
        // 3. 更新用户 ZSET 点赞列表
        LocalDateTime now = LocalDateTime.now();

        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/note_like_check_and_update_zset.lua")));

        script.setResultType(Long.class);

        result  = redisTemplate.execute(script, Collections.singletonList(userNoteLikeZSetRedisKey), noteId, DateUtils.localDateTime2Timestamp(now));

        // 若 ZSet 列表不存在，需要重新初始化
        if (Objects.equals(result, NoteLikeLuaResultEnum.NOT_EXIST.getCode())) {
            List<NoteLikeDO> noteLikeDOS = noteLikeDOMapper.selectLikeByUserIdAndLimit(userId, 100);

            long expiredSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);

            DefaultRedisScript<Long> script2 = new DefaultRedisScript<>();

            script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_like_zset_and_expire.lua")));

            script2.setResultType(Long.class);

            // 数据库中间存在点赞记录需要同步到 redis 中间
            if (CollUtil.isNotEmpty(noteLikeDOS)) {
                Object[] luaArgs = buildNoteLikeZSetLuaArgs(noteLikeDOS, expiredSeconds);

                redisTemplate.execute(script2, Collections.singletonList(userNoteLikeZSetRedisKey), luaArgs);
            } else {
                // 数据库没有点赞数据直接现在点赞的数据添加到 redis 中间
                List<Object> luaArgs = Lists.newArrayList();
                luaArgs.add(DateUtils.localDateTime2Timestamp(LocalDateTime.now()));
                luaArgs.add(noteId);
                luaArgs.add(expiredSeconds);

                redisTemplate.execute(script2, Collections.singletonList(userNoteLikeZSetRedisKey), luaArgs);
            }
        }
        // 4. 发送 MQ, 将点赞数据落库
        LikeUnlikeNoteReqDTO likeUnlikeNoteReqDTO = LikeUnlikeNoteReqDTO.builder()
                .userId(userId)
                .noteId(noteId)
                .type(LikeUnlikeNoteTypeEnum.LIKE.getCode())
                .createTime(now)
                .creatorId(creatorId)
                .build();

        // 构建信息
        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(likeUnlikeNoteReqDTO))
                .build();

        String destination = MQConstants.TOPIC_LIKE_OR_UNLIKE + ":" + MQConstants.TAG_LIKE;

        String hashKey = String.valueOf(userId);

        // 使用顺序传递信息 =》 防止多次点击之后重复关注或重复取消关注操作
        rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 【笔记点赞】MQ 发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 【笔记点赞】MQ 发送异常: ", throwable);
            }
        });

        return Response.success();
    }

    /**
     * 取消笔记点赞
     * @param unlikeNoteReqVO
     * @return
     */
    @Override
    public Response<?> unlikeNote(UnlikeNoteReqVO unlikeNoteReqVO) {
        Long noteId = unlikeNoteReqVO.getId();

        // 1.校验笔记是否存在
        Long creatorId = checkNoteIsExistAndGetCreatorId(noteId);

        // 2.校验笔记是否被点赞过
        Long userId = LoginUserContextHolder.getUserId();

        String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId);

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_unlike_check.lua")));
        script.setResultType(Long.class);

        Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId);

        NoteUnlikeLuaResultEnum noteUnlikeLuaResultEnum = NoteUnlikeLuaResultEnum.valueOf(result);

        switch (noteUnlikeLuaResultEnum) {
            // 布隆过滤器不存在
            case NOT_EXISTS -> {
                // 异步初始化布隆过滤器
                threadPoolTaskExecutor.submit(() -> {
                    // 保底一天加随机时间过期
                    long expiredSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24 );
                    batchAddNoteLike2BloomAndExpire(userId, expiredSeconds, bloomUserNoteLikeListKey);
                });
                // 查数据库校验是否点赞
                int count = noteLikeDOMapper.selectCountByUserIdAndNoteId(userId, noteId);

                if (count == 0) throw new BizException(ResponseCodeEnum.NOTE_NOT_LIKED);
            }
            case NOTE_NOT_LIKED -> throw new BizException(ResponseCodeEnum.NOTE_NOT_LIKED);
        }


        // 3. 能走到这里，说明布隆过滤器判断已点赞，直接删除 ZSET 中已点赞的笔记 ID
        // 用户点赞列表 ZSet Key
        String userNoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);

        redisTemplate.opsForZSet().remove(userNoteLikeZSetKey, noteId);

        // 4. 发送 MQ, 数据更新落库
        LikeUnlikeNoteReqDTO likeUnlikeNoteReqDTO = LikeUnlikeNoteReqDTO.builder()
                .userId(userId)
                .noteId(noteId)
                .type(LikeUnlikeNoteTypeEnum.UNLIKE.getCode())
                .createTime(LocalDateTime.now())
                .creatorId(creatorId)
                .build();

        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(likeUnlikeNoteReqDTO))
                .build();

        String destination = MQConstants.TOPIC_LIKE_OR_UNLIKE + ":" + MQConstants.TAG_UNLIKE;

        String hashKey = String.valueOf(userId);

        rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 【笔记取消点赞】MQ 发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 【笔记取消点赞】MQ 发送异常: ", throwable);
            }
        });

        return Response.success();
    }

    /**
     * 收藏笔记
     * @param collectNoteReqVO
     * @return
     */
    @Override
    public Response<?> collectNote(CollectNoteReqVO collectNoteReqVO) {
        Long noteId = collectNoteReqVO.getId();

        // 校验被收藏的笔记是否存在
        Long creatorId = checkNoteIsExistAndGetCreatorId(noteId);

        // 2. 判断目标笔记，是否已经收藏过
        Long userId = LoginUserContextHolder.getUserId();

        String bloomUserNoteCollectListKey = RedisKeyConstants.buildBloomUserNoteCollectListKey(userId);

        String userNoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();

        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_collect_check.lua")));

        script.setResultType(Long.class);

        Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteCollectListKey), noteId);

        NoteCollectLuaResultEnum noteCollectLuaResultEnum = NoteCollectLuaResultEnum.valueOf(result);

        switch (noteCollectLuaResultEnum) {
            case NOT_EXIST -> {
                int count = noteCollectionDOMapper.selectCountByUserIdAndNoteId(userId, noteId);

                long expiredSeconds = 60*60*24 + RandomUtil.randomInt(60*60*24);

                if (count > 0) {
                    threadPoolTaskExecutor.submit(() -> {
                        batchAddNoteCollect2BloomAndExpire(userId, expiredSeconds, bloomUserNoteCollectListKey);
                    });
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_COLLECTED);
                }

                //  若目标笔记未被收藏，查询当前用户是否有收藏其他笔记，有则同步初始化布隆过滤器
                batchAddNoteCollect2BloomAndExpire(userId, expiredSeconds, bloomUserNoteCollectListKey);

                //  添加当前收藏笔记 ID 到布隆过滤器中
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_add_note_collect_and_expire.lua")));
                script.setResultType(Long.class);
                redisTemplate.execute(script, Collections.singletonList(bloomUserNoteCollectListKey), noteId, expiredSeconds);


            }
            // 目标笔记已经被收藏 (可能存在误判，需要进一步确认)
            case NOTE_COLLECTED -> {
                Double score = redisTemplate.opsForZSet().score(userNoteLikeZSetKey, noteId);

                if (Objects.nonNull(score)) {
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_COLLECTED);
                }

                int count = noteCollectionDOMapper.selectNoteIsCollected(userId, noteId);

                if (count > 0) {
                    // 数据库里面有收藏记录，而 Redis 中 ZSet 已过期被删除的话，需要重新异步初始化 ZSet
                    asynInitUserNoteCollectsZSet(userId, userNoteLikeZSetKey);

                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_COLLECTED);
                }
            }
        }
        // 3. 更新用户 ZSET 收藏列表
        LocalDateTime now = LocalDateTime.now();
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/note_collect_check_and_update_zset.lua")));
        script.setResultType(Long.class);

        result = redisTemplate.execute(script, Collections.singletonList(userNoteLikeZSetKey), noteId, DateUtils.localDateTime2Timestamp(now));

        // 若 ZSet 列表不存在，需要重新初始化
        if (Objects.equals(result, NoteCollectLuaResultEnum.NOT_EXIST.getCode())) {
            List<NoteCollectionDO> noteCollectionDOS = noteCollectionDOMapper.selectCollectedByUserIdAndLimit(userId, 300);

            long expiredSeconds = 60*60*24 + RandomUtil.randomInt(60*60*24);

            DefaultRedisScript<Long> script2 = new DefaultRedisScript<>();
            script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_collect_zset_and_expire.lua")));
            script2.setResultType(Long.class);

            if (CollUtil.isNotEmpty(noteCollectionDOS)) {
                Object[] luaArgs = buildNoteCollectZSetLuaArgs(noteCollectionDOS, expiredSeconds);

                redisTemplate.execute(script2, Collections.singletonList(userNoteLikeZSetKey), luaArgs);

                // 将现在的数据插入
                redisTemplate.execute(script, Collections.singletonList(userNoteLikeZSetKey), noteId, DateUtils.localDateTime2Timestamp(now));
            } else {
                List< Object> luaArgs = Lists.newArrayList();
                luaArgs.add(DateUtils.localDateTime2Timestamp(LocalDateTime.now()));
                luaArgs.add(noteId);
                luaArgs.add(expiredSeconds);

                redisTemplate.execute(script2, Collections.singletonList(userNoteLikeZSetKey), luaArgs.toArray());
            }
        }
        // 4. 发送 MQ, 将收藏数据落库
        CollectUnCollectNoteMqDTO collectUnCollectNoteMqDTO = CollectUnCollectNoteMqDTO.builder()
                .userId(userId)
                .noteId(noteId)
                .type(CollectUnCollectNoteTypeEnum.COLLECT.getCode())
                .createTime(now)
                .creatorId(creatorId)
                .build();

        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(collectUnCollectNoteMqDTO))
                .build();

        String destination = MQConstants.TOPIC_COLLECT_OR_UN_COLLECT + ":" + MQConstants.TAG_COLLECT;

        String hashKey = String.valueOf(userId);

        // 异步发送顺序 MQ 消息，提升接口响应速度
        rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 【笔记收藏】MQ 发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 【笔记收藏】MQ 发送异常: ", throwable);
            }
        });
        return Response.success();
    }

    /**
     * 取消收藏笔记
     * @param unCollectNoteReqVO
     * @return
     */
    @Override
    public Response<?> unCollectNote(UnCollectNoteReqVO unCollectNoteReqVO) {
        Long noteId = unCollectNoteReqVO.getId();
        // 校验笔记是否存在 以及获取创建者 ID
        Long creatorId = checkNoteIsExistAndGetCreatorId(noteId);

        // 2. 校验笔记是否被收藏过
        Long userId = LoginUserContextHolder.getUserId();

        String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId);

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_uncollect_check.lua")));
        script.setResultType(Long.class);

        Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId);
        NoteUnCollectLuaResultEnum noteUnCollectLuaResultEnum = NoteUnCollectLuaResultEnum.valueOf(result);
        switch (noteUnCollectLuaResultEnum) {
            // 布隆过滤器不存在
            case NOT_EXISTS -> {
                // 异步初始化布隆过滤器
                threadPoolTaskExecutor.submit(() -> {
                    long expiredSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
                    batchAddNoteCollect2BloomAndExpire(userId, expiredSeconds, bloomUserNoteLikeListKey);
                });
                int count = noteCollectionDOMapper.selectNoteIsCollected(userId, noteId);
                // 数据库中间的数据不存在
                if (count == 0) throw new BizException(ResponseCodeEnum.NOTE_NOT_COLLECTED);
            }
            // 布隆过滤器校验目标笔记未被收藏（判断绝对正确）
            case NOTE_NOT_COLLECTED -> throw new BizException(ResponseCodeEnum.NOTE_NOT_COLLECTED);
        }


        // 3. 删除 ZSET 中已收藏的笔记 ID
        String userNoteCollectZSetKey = RedisKeyConstants.buildUserNoteCollectZSetKey(noteId);

        // 直接删除 ZSet 中间数据
        redisTemplate.opsForZSet().remove(userNoteCollectZSetKey, noteId);


        // 4. 发送 MQ, 数据更新落库
        CollectUnCollectNoteMqDTO collectUnCollectNoteMqDTO = CollectUnCollectNoteMqDTO.builder()
                .userId(userId)
                .noteId(noteId)
                .createTime(LocalDateTime.now())
                .type(CollectUnCollectNoteTypeEnum.UN_COLLECT.getCode())
                .creatorId(creatorId)
                .build();

        Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(collectUnCollectNoteMqDTO))
                .build();

        // 链接 Topic 和 Tag
        String destination = MQConstants.TOPIC_COLLECT_OR_UN_COLLECT + ":" + MQConstants.TAG_UN_COLLECT;

        String hashKey = String.valueOf(userId);

        // 异步信息发送
        rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 【笔记取消收藏】MQ发送成功， sendResult:{} ", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 【笔记取消收藏】MQ发送异常", throwable);
            }
        });

        return Response.success();
    }

    /**
     * 异步初始化用户收藏笔记 ZSet
     * @param userId
     * @param userNoteLikeZSetKey
     */
    private void asynInitUserNoteCollectsZSet(Long userId, String userNoteLikeZSetKey) {
        threadPoolTaskExecutor.execute(() -> {
            Boolean hasKey = redisTemplate.hasKey(userNoteLikeZSetKey);

            if (!hasKey) {
                // 查询用户最新关注的 300 篇笔记
                List<NoteCollectionDO> noteCollectionDOS = noteCollectionDOMapper.selectCollectedByUserIdAndLimit(userId, 300);

                if (CollUtil.isNotEmpty(noteCollectionDOS)) {
                    long expiredSeconds = 60*60*24 + RandomUtil.randomInt(60*60*24);

                    Object[] luaArgs = buildNoteCollectZSetLuaArgs(noteCollectionDOS, expiredSeconds);

                    DefaultRedisScript<Long> script2 = new DefaultRedisScript<>();
                    script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_batch_add_note_collect_and_expire.lua")));
                    script2.setResultType(Long.class);

                    redisTemplate.execute(script2, Collections.singletonList(userNoteLikeZSetKey), luaArgs);
                }
            }
        });
    }

    /**
     * 构建笔记收藏 ZSET Lua 脚本参数
     * @param noteCollectionDOS
     * @param expiredSeconds
     * @return
     */
    private static Object[] buildNoteCollectZSetLuaArgs(List<NoteCollectionDO> noteCollectionDOS, long expiredSeconds) {
        int argsLength = noteCollectionDOS.size() * 2 + 1;

        Object[] luaArgs = new Object[argsLength];

        int i = 0;
        for (NoteCollectionDO noteCollectionDO : noteCollectionDOS) {
            luaArgs[i] = DateUtils.localDateTime2Timestamp(noteCollectionDO.getCreateTime());
            luaArgs[i + 1] = noteCollectionDO.getNoteId();
            i += 2;
        }

        luaArgs[argsLength - 1] = expiredSeconds;

        return luaArgs;
    }

    /**
     * 初始化收藏布隆过滤器
     * @param userId
     * @param expiredSeconds
     * @param bloomUserNoteCollectListKey
     */
    private void batchAddNoteCollect2BloomAndExpire(Long userId, long expiredSeconds, String bloomUserNoteCollectListKey) {
        try {
            List<NoteCollectionDO> noteCollectionDOS = noteCollectionDOMapper.selectByUserId(userId);

            if (CollUtil.isNotEmpty(noteCollectionDOS)) {
                DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_batch_add_note_collect_and_expire.lua")));
                script.setResultType(Long.class);

                List< Object> luaArgs = Lists.newArrayList();
                noteCollectionDOS.forEach(noteCollectionDO -> luaArgs.add(noteCollectionDO.getNoteId()));
                luaArgs.add(expiredSeconds);
                redisTemplate.execute(script, Collections.singletonList(bloomUserNoteCollectListKey), luaArgs.toArray());
            }
        } catch (Exception e) {
            log.error("## 异步初始化【笔记收藏】布隆过滤器异常: ", e);
        }
    }

    /**
     * 异步将 redis 中间的 ZSet 进行初始化处理操作
     * @param userId
     * @param userNoteLikeZSetRedisKey
     */
    private void asynInitUserNoteLikesZSet(Long userId, String userNoteLikeZSetRedisKey) {
        threadPoolTaskExecutor.execute(() -> {
            Boolean hasKey = redisTemplate.hasKey(userNoteLikeZSetRedisKey);

            if (!hasKey) {
                // 查询用户最新点赞前一百的数据插入 zset 中间
                List<NoteLikeDO> noteLikeDOS = noteLikeDOMapper.selectLikeByUserIdAndLimit(userId, 100);

                long expiredSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);

                Object[] luaArgs = buildNoteLikeZSetLuaArgs(noteLikeDOS, expiredSeconds);

                DefaultRedisScript<Long> script2 = new DefaultRedisScript<>();
                script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_like_zset_and_expire.lua")));
                script2.setResultType(Long.class);

                redisTemplate.execute(script2, Collections.singletonList(userNoteLikeZSetRedisKey), luaArgs);
            }
        });
    }

    /**
     * 构建 lua 脚本参数
     * @param noteLikeDOS
     * @param expiredSeconds
     * @return
     */
    private static Object[] buildNoteLikeZSetLuaArgs(List<NoteLikeDO> noteLikeDOS, long expiredSeconds) {
        int argsLength = noteLikeDOS.size() * 2 + 1;

        Object[] luaArgs = new Object[argsLength];

        int i = 0;
        for (NoteLikeDO noteLikeDO : noteLikeDOS) {
            luaArgs[i] = DateUtils.localDateTime2Timestamp(noteLikeDO.getCreateTime());
            luaArgs[i + 1] = noteLikeDO.getNoteId();
            i += 2;
        }

        luaArgs[argsLength - 1] = expiredSeconds;
        return luaArgs;
    }


    /**
     * 同步将点赞笔记同步到布隆过滤器初始化
     * @param userId
     * @param expireSeconds
     * @param bloomUserNoteLikeListRedisKey
     */
    private void batchAddNoteLike2BloomAndExpire(Long userId, Long expireSeconds, String bloomUserNoteLikeListRedisKey) {
        try {
            // 全量同步一下，并设置过期时间
            List<NoteLikeDO> noteLikeDOS = noteLikeDOMapper.selectByUserId(userId);

            if (CollUtil.isNotEmpty(noteLikeDOS)) {
                DefaultRedisScript<Long> script = new DefaultRedisScript<>();
                script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/bloom_batch_add_note_like_and_expire.lua")));
                script.setResultType(Long.class);

                ArrayList<Object> luaArgs = Lists.newArrayList();
                // 将每一个查询的 note 的 noteId 数据插入参数中间使用
                noteLikeDOS.forEach(noteLikeDO -> luaArgs.add(noteLikeDO.getNoteId()));
                luaArgs.add(expireSeconds);

                redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListRedisKey), luaArgs.toArray());
            }
        } catch (Exception e) {
            log.error("## 异步初始化布隆过滤器异常: ", e);
        }
    }

    /**
     * 校验笔记是否存在，若存在，则获取笔记的发布者 ID
     * @param noteId
     * @return
     */
    private Long checkNoteIsExistAndGetCreatorId(Long noteId) {
        // 本地缓存中间
        String findNoteDetailRspVOSStrLocalCache = LOCAL_CACHE.getIfPresent(noteId);

        FindNoteDetailRspVO findNoteDetailRspVO = JsonUtils.parseObject(findNoteDetailRspVOSStrLocalCache, FindNoteDetailRspVO.class);

        if (Objects.isNull(findNoteDetailRspVO)) {
            // redis 中间
            String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);

            String noteDetailJson = redisTemplate.opsForValue().get(noteDetailRedisKey);

            findNoteDetailRspVO = JsonUtils.parseObject(noteDetailJson, FindNoteDetailRspVO.class);

            if (Objects.isNull(findNoteDetailRspVO)) {
                // 查数据库
                Long creatorId = noteDOMapper.selectCreatorIdByNoteId(noteId);

                if (Objects.isNull(creatorId)) {
                    throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
                }

                // 异步查询笔记之后存入本地缓存以及 redis 中间
                threadPoolTaskExecutor.submit(() -> {
                    FindNoteDetailReqVO findNoteDetailReqVO = FindNoteDetailReqVO.builder().id(noteId).build();

                    findNoteDetail(findNoteDetailReqVO);
                });

                return creatorId;
            }
        }

        return findNoteDetailRspVO.getCreatorId();
    }


    /**
     * 校验笔记是否可见
     * @param visible 是否可见
     * @param currUserId 查看笔记用户
     * @param creatorId 笔记创作者
     */
    private void checkNoteVisible(Integer visible, Long currUserId, Long creatorId) {
        if (Objects.equals(visible, NoteVisiableEnum.PRIVATE.getCode())
                && !Objects.equals(currUserId, creatorId)) {
            throw new BizException(ResponseCodeEnum.NOTE_PRIVATE);
        }
    }

    /**
     * 校验笔记的可见性(针对 VO 实体类)
     * @param userId
     * @param findNoteDetailRspVO
     */
    private void checkNoteVisibleFromVO(Long userId, FindNoteDetailRspVO findNoteDetailRspVO){
        if (Objects.nonNull(findNoteDetailRspVO)) {
            Integer visible = findNoteDetailRspVO.getVisible();
            checkNoteVisible(visible, userId, findNoteDetailRspVO.getCreatorId());
        }
    }
}
